﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Jsdl.Domain;
using Jsdl.Domain.Input.JsonService;
using Jsdl.Domain.Intermediate;
using Jsdl.Domain.Output;
using Jsdl.Domain.Type;
using Newtonsoft.Json;

namespace Jsdl.Generators
{
	public abstract class CodeGenerator
    {
        private int _anonymousTypesCount = 0;
        
        public static CodeGenerator Create(string outputType)
		{
			var className = "Jsdl.Generators." + outputType + "CodeGenerator";
			
            var type = Type.GetType(className);
			if (type != null)
				return Activator.CreateInstance(type) as CodeGenerator;
			
            throw new Exception("Output type '" + outputType + "' could not be found!");
		}

	    public string LoadTemplate(string name)
	    {
	        var assembly = GetType().Assembly;
            var resourceName = "Jsdl.Templates." + GetType().Name + "." + name + ".tpl";

            using (Stream stream = assembly.GetManifestResourceStream(resourceName))
            using (StreamReader reader = new StreamReader(stream))
                return reader.ReadToEnd();
	    }

		// options
		public bool AddGetOperationPrefix { get; set; }
		public bool DisableCodeGeneration { get; set; }

		// abstract
        /// <summary>Must have a [classes] placeholder. </summary>
        public abstract string GenerateJsonServiceCode();
        /// <summary>Must have a [body] placeholder. </summary>
        public abstract string GenerateJsonServiceClassCode();

		public abstract string GenerateJsonServiceOperationCode(string operationName, JsdlOperation operation);

		public abstract string GenerateClassCode(string outputTypeName, string outputTypeBody);
		public abstract string GeneratePropertyCode(string propertyName, string targetPropertyType, JsdlProperty property);

		public abstract string GetTypeName(JsdlType type);
		public abstract string GetTypeName(string className);

		// state
		public string ClientClassName { get; set; }
		public string NamespaceName { get; set; }
		public List<GeneratedItem> Classes { get; set; }

        #region JSON Web Service generation

        public string GenerateServiceClientCodeFromUrl(string url, string clientClassName = "UnknownServiceClient", string namespaceName = "UnknownNamespace")
        {
            using (var client = new WebClient())
                return GenerateServiceClientCodeFromJson(client.DownloadString(url), clientClassName, namespaceName);
        }

        public string GenerateServiceClientCodeFromJson(string json, string clientClassName = "UnknownServiceClient", string namespaceName = "UnknownNamespace")
        {
            var service = JsonConvert.DeserializeObject<JsdlService>(json);
            return GenerateServiceClientCode(service, clientClassName, namespaceName);
        }

        public string GenerateServiceClientCode(JsdlService service, string clientClassName = "UnknownServiceClient", string namespaceName = "UnknownNamespace")
        {
            ClientClassName = clientClassName;
            NamespaceName = namespaceName;

            Classes = new List<GeneratedItem>();

            if (DisableCodeGeneration)
                return "";

            return GenerateJsonServiceCode()
                .Replace("[classes]", GenerateClasses(service))
                .Replace("\r", "")
                .Replace("\n", Environment.NewLine);
        }

        private string GenerateClasses(JsdlService service)
        {
            if (service.Types != null)
            {
                foreach (var type in service.Types)
                    CreateOutputType(type, "unknown");
            }

            GenerateServiceClientClass(service);

            var result = "";
            foreach (var c in Classes)
                result = result + IntendCode(c.Code);
            return result;
        }

        private void GenerateServiceClientClass(JsdlService service)
        {
            if (service.Operations != null)
            {
                var code = "";
                foreach (var op in service.Operations)
                {
                    var operation = op.Value;
                    var operationName = op.Key;

                    if (AddGetOperationPrefix && operation.Transport == JsdlOperationTransport.get)
                        operationName = "Get" + ConvertName(operationName);

                    if (operation.Parameter != null) // POST only
                    {
                        if (operation.Transport == JsdlOperationTransport.get)
                            throw new Exception("'parameter' only available with POST. Operation: " + operationName);
                    }

                    code += GenerateJsonServiceOperationCode(operationName, operation);
                }

                Classes.Add(new GeneratedItem
                {
                    Name = "_",
                    Code = GenerateJsonServiceClassCode().Replace("[body]", IntendCode(code))
                });
            }
        }

        #endregion

        public string GenerateType(JsdlType type)
        {
            bool isPrimitiveType;
            _anonymousTypesCount++;
            return GenerateType(type, "Anonymous" + _anonymousTypesCount, out isPrimitiveType);
        }

        public string GenerateType(JsdlType type, string suggestedClassName)
        {
            bool isPrimitiveType;
            return GenerateType(type, suggestedClassName, out isPrimitiveType);
        }

        protected virtual string GenerateType(JsdlType type, string suggestedClassName, out bool isPrimitiveType)
        {
            if (type.Type == "object" || type.Type == null)
            {
                isPrimitiveType = false;
                var cla = CreateOutputType(type, suggestedClassName);
                return cla.Name;
            }

            if (type.Type == "array" || type.Type == "dictionary")
            {
                isPrimitiveType = false;
                var itemType = GenerateType(type.Items, suggestedClassName);
                return GetArrayTypeName(type.Type, itemType);
            }

            var className = GetTypeName(type.Type);
            if (Classes.Any(c => c.Name == className))
            {
                isPrimitiveType = false;
                return className;
            }

            isPrimitiveType = true;
            return GetTypeName(type);
        }


        public GeneratedItem CreateOutputType(JsdlType inputType, string suggestedOutputTypeName)
		{
			var className = GetTypeName(inputType.Name ?? suggestedOutputTypeName);
            var classBody = GeneratePropertiesCode(className, inputType);

            var item = new GeneratedItem
            {
                Name = className,
                Code = GenerateClassCode(className, IntendCode(classBody))
            };

            Classes.Add(item);
            return item;
		}

		/// <summary>
		/// listType is either "array" or "dictionary"
		/// </summary>
		public abstract string GetArrayTypeName(string listType, string itemType);

		public string GeneratePropertiesCode(string className, JsdlType type)
		{
			var output = "";
			if (type.Properties != null)
			{
				foreach (var prop in type.Properties)
				{
					var propertyName = prop.Key;
					var property = prop.Value;
					var typeName = GenerateType(property, className + ConvertName(propertyName, false, true));
					
                    output += GeneratePropertyCode(propertyName, typeName, property);
				}
			}
			return output;
		}

		public string ConvertName(string name, bool lowerStart = false, bool convertToSingular = false)
		{
			name = lowerStart ?
				name[0].ToString(CultureInfo.InvariantCulture).ToLower() + name.Substring(1) :
				name[0].ToString(CultureInfo.InvariantCulture).ToUpper() + name.Substring(1);

			if (convertToSingular && name.Last() == 's')
				name = name.Substring(0, name.Length - 1);

			int output;
			if (int.TryParse(name, out output))
				name = (lowerStart ? "num" : "Num") + output; 

			return name;
		}

		public string IntendCode(string text, int tabCount = 1)
		{
			var tabs = "";
			for (var i = 0; i < tabCount; i++)
				tabs += "\t";
			return tabs + text.Replace("\n", "\n" + tabs) + "\n";
		}
	}
}
